package c.c.d.s.s.o.ts.rs.configuration;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;

/**
 * 资源服务器配置
 *
 * @author LiKe
 * @version 1.0.0
 * @date 2020-06-13 20:55
 */
@Slf4j
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    private static final String RESOURCE_ID = "resource-server";

    private static final String AUTHORIZATION_SERVER_CHECK_TOKEN_ENDPOINT_URL = "http://localhost:18957/token-customize-authorization-server/oauth/check_token";

    // =================================================================================================================

    private AuthenticationEntryPoint authenticationEntryPoint;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // @formatter:off
        resources.resourceId(RESOURCE_ID).tokenServices(remoteTokenServices()).stateless(true);

        resources.authenticationEntryPoint(authenticationEntryPoint);
        // @formatter:on
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().anyRequest().authenticated();
    }

    // -----------------------------------------------------------------------------------------------------------------

    /**
     * Description: 远端令牌服务类<br>
     * Details: 调用授权服务器的 /oauth/check_token 端点解析令牌. <br>
     * 在本 DEMO 中, 调用授权服务器的 {@link org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint} 端点, <br>
     * 将私钥签名的 JWT 发到授权服务器, 后者用公钥验证 Signature 部分
     *
     * @return org.springframework.security.oauth2.provider.token.RemoteTokenServices
     * @author LiKe
     * @date 2020-07-22 20:33:13
     */
    private RemoteTokenServices remoteTokenServices() {
        final RemoteTokenServices remoteTokenServices = new RemoteTokenServices();

        // ~ 设置 RestTemplate, 以自行决定异常处理
        final RestTemplate restTemplate = new RestTemplate();
        restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
            @Override
            // Ignore 400
            public void handleError(ClientHttpResponse response) throws IOException {
                final int rawStatusCode = response.getRawStatusCode();
                System.out.println(rawStatusCode);
                if (rawStatusCode != 400) {
                    final String responseData = new String(super.getResponseBody(response));
                    // ~ 默认情况下会调用超类的 handleError 方法, 后者最终会抛出异常导致前端跳转到 /error 页面. 在前后端分离的场景下, 只期望资源服务区给它自己的前端返回 JSON 格式的数据.
                    //   ~ 分析
                    //     用户定义的 RemoteTokenServices 会在 OAuth2AuthenticationProcessingFilter#doFilter 方法中, 被 AuthenticationManager.authenticate 方法调用,
                    //     其中 AuthenticationManager 的真实类型是 OAuth2AuthenticationManager, 其 authenticate 方法会调用 tokenServices (当前场景下, 就是我们定义的 RemoteTokenServices) 的 loadAuthentication,
                    //     而如果这个 tokenServices 的真实类型是 RemoteTokenServices, 则会触发资源服务器去请求授权服务器的 /oauth/check_token 端点解析令牌的操作.
                    //     所以在这一步, 如果令牌过期或是无效, 授权服务器的响应会传回给资源服务器, 如何处理这个响应, 就是我们这里需要考虑的内容
                    //   ~ 方案
                    //     由于整个调用链的上层是 OAuth2AuthenticationProcessingFilter, 通过查看源码我们知道, 如果认证过程中抛出 OAuth2Exception, 会被 AuthenticationEntryPoint 处理,
                    //
                    // super.handleError(response, response.getStatusCode());
                    throw new OAuth2Exception(responseData);
                }
            }
        });
        remoteTokenServices.setRestTemplate(restTemplate);

        // ~ clientId 和 clientSecret 会以 base64(clientId:clientSecret) basic 方式请求授权服务器
        remoteTokenServices.setClientId(RESOURCE_ID);
        remoteTokenServices.setClientSecret("resource-server-p");

        // ~ 请求授权服务器的 CheckTokenEndpoint 端点解析 JWT (AuthorizationServerEndpointsConfigurer 中指定的 tokenServices.
        //   实现了 ResourceServerTokenServices 接口,
        //   如果没有, 则使用默认的 (org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerEndpointsConfiguration.checkTokenEndpoint)
        remoteTokenServices.setCheckTokenEndpointUrl(AUTHORIZATION_SERVER_CHECK_TOKEN_ENDPOINT_URL);
        return remoteTokenServices;
    }

    // -----------------------------------------------------------------------------------------------------------------

    @Autowired
    public void setAuthenticationEntryPoint(@Qualifier("customAuthenticationEntryPoint") AuthenticationEntryPoint authenticationEntryPoint) {
        this.authenticationEntryPoint = authenticationEntryPoint;
    }
}
